var canvas = document.getElementById("gameCanvas") as HTMLCanvasElement;
var context = canvas.getContext("2d")


/**
 * 使用有限状态机对游戏逻辑进行梳理
 * 扩展阅读：使用有限状态机的优势是什么？
 * https://zhuanlan.zhihu.com/p/22976065
 */
namespace fsm {


    export class StateMachine {

        private currentState?: State;

        replaceState(state: State) {
            if (this.currentState) {
                this.currentState.onExit();
            }
            this.currentState = state;
            this.currentState.onEnter();
        }

        update() {
            if (this.currentState) {
                this.currentState.onUpdate();
            }
        }
    }

    export class State {

        onEnter() {

        }

        onUpdate() {

        }

        onExit() {

        }

    }



}

/**
 * 使用命令模式对用户操作执行结果进行封装
 * 扩展阅读：命令模式的使用场景都有哪些？
 * https://www.cnblogs.com/tangyikejun/p/5931571.html
 */
namespace command {


    export class Command {


        execute(notifyFinish: Function) {

        }

    }


    export class CommandPool {

        private list: Command[] = [];

        clear() {
            this.list = [];
        }

        add(command: Command) {
            this.list.push(command);
        }

        execute() {

            const self = this;


            function onFinish() {
                setTimeout(function () {
                    self.execute();
                }, 1);
            }

            let command = this.list.shift();
            if (command) {
                command.execute(onFinish);
            }
        }
    }

}


class EventDispatcher {

    private listeners: { type: string, callback: Function }[] = [];

    dispatchEvent(type: string, eventData: any) {
        for (let listener of this.listeners) {
            if (type == listener.type) {
                listener.callback(eventData)
            }
        }
    }

    addEventListener(type: string, callback: Function) {
        this.listeners.push({ type: type, callback: callback });
    }

    removeEventListener(type: string, callback: Function) {
        for (let listener of this.listeners) {
            if (listener.type == type && listener.callback == callback) {
                const index = this.listeners.indexOf(listener);
                this.listeners.splice(index, 1);
                break;
            }
        }
    }

}


var disptcher = new EventDispatcher();
disptcher.addEventListener("click", (eventData: any) => {
    console.log(eventData)
})
disptcher.dispatchEvent("click", { message: 'helloworld' })



namespace renderer {

    export abstract class DisplayObject extends EventDispatcher {

        x = 0;

        y = 0;

        scaleX = 1;

        scaleY = 1;

        rotation = 0;

        alpha = 1;

        globalAlpha = 1;

        globalMatrix: math.Matrix = new math.Matrix();

        localMatrix: math.Matrix = new math.Matrix();

        parent: DisplayObjectContainer | null = null;

        visible: boolean = true;

        constructor(x: number = 0, y: number = 0) {
            super();
            this.x = x;
            this.y = y;
        }

        abstract render(context: CanvasRenderingContext2D): void;

        draw(context: CanvasRenderingContext2D) {

            if (!this.visible) {
                return;
            }

            const parentGlobalAlpha = this.parent ? this.parent.globalAlpha : 1;
            this.globalAlpha = parentGlobalAlpha * this.alpha;
            context.globalAlpha = this.globalAlpha;

            this.localMatrix.updateFromDisplayObject(this.x, this.y, this.scaleX, this.scaleY, this.rotation);
            const parentGlobalMatrix = this.parent ? this.parent.globalMatrix : new math.Matrix();
            const globalMatrix = math.matrixAppendMatrix(this.localMatrix, parentGlobalMatrix);
            this.globalMatrix = globalMatrix;
            context.setTransform(globalMatrix.a, globalMatrix.b, globalMatrix.c, globalMatrix.d, globalMatrix.tx, globalMatrix.ty);

            this.render(context);
        }


        hitTest(x: number, y: number): null | DisplayObject {
            return null;
        }

    }

    export class Bitmap extends DisplayObject {

        constructor(x: number, y: number, image: HTMLImageElement) {
            super(x, y);
            this.image = image;
        }

        image: HTMLImageElement | null = null;

        render(context: CanvasRenderingContext2D) {
            const parentGlobalAlpha = this.parent ? this.parent.globalAlpha : 1;
            if (this.image) {
                context.drawImage(this.image, 0, 0)
            }
        }

        hitTest(x: number, y: number) {

            const width = this.image ? this.image.width : 0;
            const height = this.image ? this.image.height : 0;
            const rect = { x: 0, y: 0, width: width, height: height };
            if (x > rect.x &&
                x < rect.x + rect.width &&
                y > rect.y &&
                y < rect.y + rect.height
            ) {
                return this;
            }
            else {
                return null;
            }
        }
    }


    /**
     * TODO:支持 SpriteSheet 实现的动画
     */
    export class Animation extends DisplayObject {


        constructor() {
            super();
        }


        render(context: CanvasRenderingContext2D) {

        }
    }

    /**
     * 支持换行
     */
    export class TextField extends DisplayObject {

        text = "";

        private measureWidth: number;

        private fontSize = 20;
        style = "16px Ariel";


        private texts: string[] = [];



        render(context: CanvasRenderingContext2D) {
            context.font = this.style;
            this.texts = [];
            // context.fillText(this.text, 0, 20);//magic number
            this.texts = this.text.split(" ");
            for (let i = 0; i < this.texts.length; i++) {
                context.fillText(this.texts[i], 0, 20 + this.fontSize * i)
            }

            // this.index = this.n.length;
            // if (this.text != "") {
            //     console.log(this.texts);
            // }

            // context.fillText(this.n[0], 50, 50);
            // console.log(this.text)

            // for (let i = 0; i < this.index; i++) {
            //     context.fillText(this.texts[i], 30, 30 + i * 20);
            // }


            this.measureWidth = context.measureText(this.text).width;
        }

        hitTest(x: number, y: number) {

            const width = this.measureWidth;
            const height = this.fontSize;
            const rect = { x: 0, y: 0, width: width, height: height };
            if (x > rect.x &&
                x < rect.x + rect.width &&
                y > rect.y &&
                y < rect.y + rect.height
            ) {
                return this;
            }
            else {
                return null;
            }
        }
    }

    export class DisplayObjectContainer extends DisplayObject {


        private children: DisplayObject[] = [];

        addChild(child: DisplayObject) {
            this.children.push(child);
            child.parent = this;
        }

        removeChild(child: DisplayObject) {
            const index = this.children.indexOf(child);
            if (index != -1) {
                this.children.splice(index, 1);
            }
        }

        removeAllChildren() {
            this.children = [];
        }

        render(context: CanvasRenderingContext2D): void {
            const renderList = this.children;

            for (let render of renderList) {
                render.draw(context);
            }
        }

        hitTest(x: number, y: number) {
            const displayObjectList = this.children;
            let hitTestResult: DisplayObject | null = null;

            for (let i = displayObjectList.length - 1; i >= 0; i--) {
                const childDisplayObject = displayObjectList[i];
                const childLocalMatrix = childDisplayObject.localMatrix;
                const childInvertLocalMatrix = math.invertMatrix(childLocalMatrix);
                const pointRelativeFromChildDisplayObject = math.pointAppendMatrix(new math.Point(x, y), childInvertLocalMatrix);
                const result = childDisplayObject.hitTest(pointRelativeFromChildDisplayObject.x, pointRelativeFromChildDisplayObject.y);
                if (result != null) {
                    hitTestResult = result;
                    break;
                }
            }
            return hitTestResult;
        }
    }





    export class Stage extends DisplayObjectContainer {

        constructor() {
            super(0, 0);
        }
    }
}



const stage = new renderer.Stage();
var gameFsm = new fsm.StateMachine();




function onTicker(context: CanvasRenderingContext2D) {
    gameFsm.update();
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.save();
    stage.draw(context);
    context.restore();
}



function enterFrame() {
    if (!context) {
        return;
    }
    onTicker(context);
    requestAnimationFrame(enterFrame);
}

canvas.onclick = function (event) {
    const globalX = event.offsetX;
    const globalY = event.offsetY;

    let hitResult = stage.hitTest(globalX, globalY);
    if (hitResult) {
        hitResult.dispatchEvent("click", { target: hitResult, globalX: globalX, globalY: globalY });
        while (hitResult.parent) {
            hitResult = hitResult.parent;
            const globalMatrix = hitResult.globalMatrix;
            const invertGlobalMatrix = math.invertMatrix(globalMatrix);
            const localPoint = math.pointAppendMatrix(new math.Point(globalX, globalY), invertGlobalMatrix);
            const eventData = {
                target: hitResult,
                globalX: globalX,
                globalY: globalY,
                localX: localPoint.x,
                localY: localPoint.y
            }
            hitResult.dispatchEvent("click", eventData);
        }
    }
}


requestAnimationFrame(enterFrame);
